--[[ Copyright 2012, Chris Ho                               ]]--
--[[ MIT License (Please read the LICENSE file for details) ]]--

Use("common/Const");
Use("common/Log");
Use("common/List");
Use("common/Position");
Use("common/Task");
Use("common/Creature");
Use("common/Player");
Use("common/Clique");
Use("common/Skill");
Use("common/Countdown");

-- The defintion for this controllable pet global:
-- id: creature game ID
-- what: the object type (always OBJECT_PET)
-- maxhp: maximum health
-- maxsp: maximum spell points
--
-- owner: pet owner table
-- friends: a table of IDs friendly to this pet
-- destination: desired position destination
-- target: DESIRED target creature/item ID (NOT ALWAYS ACTUAL TARGET)
-- leader: current leader ID (normally the owner)
-- combo: combo index (multi-task series)
-- timer: a generic countdown timer
-- task: current task snapshot
-- mode: current behavior mode
Pet = {};

-- Pet States/Dispositions
MODE_STOP = 0;
MODE_MOVE = 1;
MODE_FOLLOW = 2;
MODE_ESCORT = 3;
MODE_PATROL = 4;
MODE_ASSAULT = 5;
MODE_DEFEND = 6;
MODE_AVOID = 7;
MODE_DEFAULT = MODE_ESCORT;

-- Generate a new pet template. The pet can not function without
-- a personality. You must install a personality (based on pet type).
-- By default, the pet is in an escort mode.
function Pet.Create(_id)
  local instance = Creature.Create(_id);

  instance.what = OBJECT_PET;
  instance.owner = Player.Create(GetV(V_OWNER, _id));
  instance.friends = Clique.Load(FRIENDS_FILE);
  instance.destination = Position.Find(instance.owner.id);
  instance.target = 0;
  instance.leader = instance.owner.id;
  instance.combo = 0;
  instance.timer = Countdown.Create(0);
  instance.task = Task.Create(instance);
  instance.mode = MODE_DEFAULT;

  instance.Behave = Pet.Behave;
  instance.React = Pet.React;
  instance.Think = Pet.Think;
  instance.RateTarget = Pet.RateTarget;
  instance.RateArea = Pet.RateArea;
  instance.Measure = Pet.Measure;
  instance.FindAssistants = Pet.FindAssistants;
  instance.FindFriends = Pet.FindFriends;
  instance.AddFriend = Pet.AddFriend;
  instance.FetchFriend = Pet.FetchFriend;
  instance.RemoveFriend = Pet.RemoveFriend;
  instance.GetLeash = Pet.GetLeash;
  instance.GetTranquility = Pet.GetTranquility;
  instance.GetResilience = Pet.GetResilience;

  return instance;
end



--[[ Change pet mode if necessary. ]]--
function Pet.Behave(self, _mode)
  if (self.mode ~= _mode) then
    Info("Pet.Behave() changing from " .. self.mode .. " to " .. _mode .. " mode");
    self.destination = Position.Find(self.owner.id);
    self.target = 0;
    self.leader = self.owner.id;
    self.combo = 0;
    self.mode = _mode;
  end

  -- Changing the behavior mode forces the existing task to end.
  self.task:Terminate();
end

--[[ React to the current situation ]]--
-- Between actions, the pet may want to react to the environment. We may
-- change mode. This function allows the pet to act on its own.
function Pet.React(self)
  -- Clear the target if it's dead.
  if (GetV(V_MOTION, self.target) == MOTION_DEAD) then
    Info("Pet.React() target " .. self.target .. " has died.");
    self.target = 0;
  end
end

--[[ Decide what to do based on the situation ]]--
-- We may change mode. This function allows the pet to act on its own.
function Pet.Think(self)
  -- NOTE: ALL BEHAVIORAL FUNCTIONS GO IN PERSONALITY FILES.
  Debug("Pet.Think()");
end


--[[ Rate a Target ]]--
-- The rating determines target preferences. A higher score indicates a more
-- preferable target. By default, rating depends on distance.
function Pet.RateTarget(self, _id)
  local here = Position.Find(self.id);
  local there = Position.Find(_id);
  local range = Position.Measure(here, there);
  local score = VISIBILITY / 4;

  -- Score is based on distance, health, and damage, with health/damage
  -- being more important than distance.

  -- Clamp range modifier to 1.
  if (range > 1) then local score = score / range end

--[[
    -- This block may not be possible (we may not get monster stats)
    local health = GetV(V_HP, foe.id);
    local damage = GetV(V_MAXHP, foe.id) - health;
    if (health < 1) then
      health = 1;
      Alert("Bad Health for " .. foe.id .. ": " .. health);
    end
    if (damage < 0) then
      damage = 0;
      Alert("Bad Damage for " .. foe.id .. ": " .. damage);
    end
    score = score + health + damage / health;
]]--

  return score;
end

--[[ Rate an Area ]]--
-- The rating determines quality of an area for an offensive effect over
-- the entire area. A higher score indicates a more preferable area.
function Pet.RateArea(self, _destination, _range)
  local score = 0;
  local index, value;

  -- Find neighbors.
  for index, value in ipairs(ACTORS) do
    -- Skip ourselves in the search.
    if (value ~= self.id) then
      local there = Position.Find(value);
      if (Position.Measure(_destination, there) <= _range) then
        -- Object is in range. Skip the neighbor if he's our friend.
        if (self:FetchFriend(value) == nil) then
          -- Each candidate starts with a small bonus to the score.
          local bonus = 0.25;

          -- If an ally is attacking the candidate, boost the score.
          if (self:FindAssistants(value).n > 0) then bonus = 1.0 end

          -- Consider each neighbor based on the intended target. If the
          -- neighbor has a target, we have to adjust the bonus.
          local intent = GetV(V_TARGET, value);
          if (intent ~= 0) then
            -- It has a target! Unless we know the target, reduce the bonus.
            bonus = -1.0;

            -- It's attacking a friend!
            if (self:FetchFriend(intent) ~= nil) then bonus = 1.25 end
            -- It's attacking our owner!
            if (intent == self.owner.id) then bonus = 1.5 end
            -- It's attacking us personally!
            if (intent == self.id) then bonus = 1.75 end
          end

          -- Update the base score.
          score = score + bonus;
        end
      end
    end
  end

  return score;
end

--[[ Find distance to owner ]]--
function Pet.Measure(self)
  return Position.Measure(Position.Find(self.id), Position.Find(self.owner.id));
end

--[[ Gather a list of friends that are assisting on a target ]]--
function Pet.FindAssistants(self, _id)
  local results = {};
  local key, value;

  -- Force an empty list.
  results.n = 0;

  -- Check us and our owner first.
  -- NOTE: We don't consider ourselves because we're trying to find out who
  -- ELSE is attacking.
--[[
  if (GetV(V_TARGET, self.id) == _id) then
    results.n = results.n + 1;
    results[results.n] = self.id;
    Debug(_id .. " Assistant: Self " .. self.id);
  end
]]--
  if (GetV(V_TARGET, self.owner.id) == _id) then
    results.n = results.n + 1;
    results[results.n] = self.owner.id;
    Debug(_id .. " Assistant: Owner " .. self.owner.id);
  end

  -- Now check our allies.
  for key, value in ipairs(self.friends) do
    if (GetV(V_TARGET, key) == _id) then
      results.n = results.n + 1;
      results[results.n] = key;
      Debug(_id .. " Assistant: Friend " .. key);
    end
  end

  return results;
end
--[[ Look for friends within 1 tile of the position ]]--
-- We automatically add the friends. Creatures must not be hostile.
-- Note that this function is only valid when we're in a stop mode.
function Pet.FindFriends(self, _list, _location)
  if (self.mode ~= MODE_STOP) then
    Alert("Stop first before finding friends! (" .. self.mode .. ")");
    return;
  end

  local index, value;
  for index, value in ipairs(_list) do
    local position = Position.Find(value);
    if (Position.Measure(_location, position) <= 1) then
      -- Target is within 1 tile.
      if (IsMonster(value) == true) then Info(value .. " is monster!")
      else Info(value .. " is something else!") end

      -- Skip ourselves and our owner.
      if ((value ~= self.id) and (value ~= self.owner.id)) then
        if (self:FetchFriend(value) == nil) then
          -- Try to make friends!
          self:AddFriend(value);
        else
          -- If he's a friend, take him off the list.
          self:RemoveFriend(value);
        end
      end
    end
  end
end
--[[ Add a friend ]]--
-- We don't add hostile creatures.
function Pet.AddFriend(self,_id)
  if (_id == self.id) then
    Alert("Not adding self as friend.");
    return;
  end
  if (_id == self.owner.id) then
    Alert("Not adding owner as friend.");
    return;
  end

  local target = GetV(V_TARGET, _id);
  if ((target == self.id) or (target == self.owner.id)) then
    Alert(_id .. " can't be friends (hostile)");
    self.friends[_id] = nil;
  else
    -- TODO: maybe we should check against current friend list.
    Info("In relationship with " .. _id);
    self.friends[_id] = Creature.Create(_id);
  end
end
--[[ Fetch a friend from our friends list ]]--
function Pet.FetchFriend(self, _id)
  -- We're always friends with ourselves and our owner.
  if (_id == self.id) then return self end
  if (_id == self.owner.id) then return self.owner end

  return self.friends[_id];
end
--[[ Remove a friend ]]--
function Pet.RemoveFriend(self, _id)
  Info("Breaking up with " .. _id);
  self.friends[_id] = nil;
end

--[[ Get the applied following distance ]]--
function Pet.GetLeash(self)
  -- When following, we've "arrived" when we're within tranquility range.
  local range = self:GetTranquility();
  if (range > LEASH) then range = LEASH end
  if (range > VISIBILITY) then range = VISIBILITY end
  return range;
end
--[[ Retrieve the actual pet tranquility stat ]]--
function Pet.GetTranquility(self)
  return TRANQUILITY;
end
--[[ Retrieve the actual pet resilience stat ]]--
function Pet.GetResilience(self)
  return RESILIENCE;
end
